package org.tri.swing;

import java.awt.Color;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.*;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JLabel;

/**
 * La clase {@code AbstractEditableLabel} es la base de la jerarquia de 
 * componentes swings denominados EditableLabels. Estos componentes se 
 * comportan como labels a la hora de mostrar información, pero pueden ser
 * configurados para permitir la edición de los mismos al hacerle click.
 * Cada subclase define diferentes métodos de edición.
 * @author kiira
 * @version 1.0, Feb 11, 2012
 */
public abstract class AbstractEditableLabel<K extends JComponent> extends JLabel {
    /** Componente que se utiliza para la edición del contenido del label. */
    protected K editableComponent;
    /** Valor por default que el label utiliza. */
    protected String defaultValue;
    /** Componente padre donde este componente se muestra. */
    protected JComponent container;
    /** Restricciones de gui dispuestas en el componente padre. */
    protected GridBagConstraints constraints;
    /** Valor default de la restricción "fill". */
    protected int defaultFillValue;
    /** Bandera que indica si el contenido del label es diferente al default. */
    private boolean modified;
    
    /**
     * Constante que indica la señal que se envia a los property listeners
     * cuando se modifica el valor del label.
     */
    public static final String MODIFIED_PROPERTY = "modified";

    /**
     * Inicializa el label. Por defecto, el componente es focusable para que
     * el mismo pueda interactuar con otros componentes, y su valor por defecto
     * es la cadena vacia.
     */
    public AbstractEditableLabel() {
        setFocusable( true );
        defaultValue = "";

    }//end constructor
    
    /**
     * {@inheritDoc}
     */
    @Override
    public void setText( String text ) {
        //si el componente editable esta setado, pasamos el texto por el previo
        //a setearlo en el label
        if ( editableComponent != null )
            text = preCheck( text );
        
        //setea el texto al label
        super.setText( text );
        
        //comprobamos si el texto esta vacio
        if ( editableComponent != null )
            checkEmptyLabel();
       
    }//end método setText

    /**
     * Setea el valor de la propiedad modified.
     * @param newValue valor a setear.
     */
    protected void setModified( boolean newValue ) {
        modified = newValue;

        //lanzamos el evento para indicar que se modifico la propidad
        firePropertyChange( MODIFIED_PROPERTY, !modified, modified );

    }//end método setModified

    /**
     * Indica si el valor del label fué o no modificado con respecto al valor
     * default.
     * @return <li>true: el valor fue modificado.<li>false: el valor no se 
     * modifico
     */
    public boolean isModified() {
        return modified;

    }//end método isModified

    /**
     * Indica si este componente esta en modo de edición en el momento de 
     * la llamada al método.
     * @return <li>true: el copmonente esta en modo de edición. <li>false: el
     * componente no esta en modo de edición.
     */
    public boolean isBeingEdited() {
        return container.isAncestorOf( editableComponent );

    }//end método isBeingEdited

    /**
     * Permite definir el valor por defecto para el componente.
     * @param value valor a setear como valor por defecto.
     */
    public void setDefaultValue( String value ) {
        if ( value != null ) {
            setText( value );

            this.defaultValue = getText();

        }//end if
        
    }//end método setDefaultValue

    /**
     * Devuelve el valor por defecto del componente.
     * @return valor por defecto del componente.
     */
    public String getDefaultValue() {
        return defaultValue;

    }//end método getDefaultValue
    
    /**
     * Este método debe ser llamado para permitir que el label sea modificado.
     * Si este método no se llama luego de inicializar el componente, el mismo
     * sera un simple label sin capacidad de modificación.
     * @param container componente padre (contenedor) de este componente.
     */
    public void makeEditable( ) {
        //el componente editable debe ser inicializado por la subclase antes
        //de poder llamar al método de la clase padre
        if ( editableComponent == null )
            throw new IllegalStateException( "El componente editable no esta "
                                             + "inicializado correctamente." );

        //bandera que indica si el componente padre es un JComponent
        boolean jcomp = true;
        
        //intentamos obtener el componente padre y castearlo
        try {
            container = (JComponent)getParent();
        
        } catch ( ClassCastException cex ) {
            jcomp = false;
            
        }
        
        //si no se consiguio el container Ó el container no es de la clase 
        //JComponent, lanzamos una excepción
        if ( container == null || !jcomp ) 
            throw new IllegalStateException( "El componente debe agregarse a"
                    + " un JComponente antes de poder hacerlo editable." );                    

        //intentamos obtener las constraints. Si el layout no es un 
        //GridBagLayout, lanzamos una excepción
        try {
            constraints =
            ( (GridBagLayout)container.getLayout() ).getConstraints( this );

        } catch ( ClassCastException cex ) {
            throw new IllegalStateException( "Este componente (por el momento)"
                    + " solo funcion con GridBagLayout." );
            
        }
        
        //seteamos el valor default (seteado por el usuario) de la constraint 
        //sfill
        defaultFillValue = constraints.fill;
        
        //comprobamos si el label esta vacio
        checkEmptyLabel();

        //seteamos la font a la misma del label
        editableComponent.setFont( getFont() );
                
        //agregamos los listeners que manejan la transición de editor a label
        editableComponent.addFocusListener( new FocusHandler() );
        editableComponent.addKeyListener( new KeyHandler() );

        //agregamos los listeners que manejan la transición de label a editor
        addMouseListener( new ClickHandler() );

    }//end método makeEditable

    /**
     * Método auxiliar que se recomienda utilizar a la hora de setear el texto
     * editado en el componente de edición. Este método se encarga de comprobar
     * si el texto es diferente al default y setear la propiedad modified 
     * acorde a esto
     * @param newText texto a aplicar al label. 
     */
    protected void applyEdition( String newText ) {
        if ( !newText.equals( getText() ) )
            if ( !defaultValue.equals( newText ) ) {
                super.setText( newText );
                setModified( true );

            } else {
                super.setText( defaultValue );
                setModified( false );

            }
                
        checkEmptyLabel();

    }//end método applyEdition

    /**
     * Devuelve el texto del label a su valor por defecto.
     */
    public void resetToDefault() {
        setText( defaultValue );

    }//end método resetToDefault
    
    /**
     * Comprueba si el label esta vacio y modifica las constraints acorde
     * a esto para evitar que el mismo colapse.
     */
    protected void checkEmptyLabel() {
        if ( getText().length() == 0 ) {
            constraints.fill = GridBagConstraints.BOTH;
            this.setBorder( 
                    BorderFactory.createLineBorder(new Color(0, 0, 0)) );
                           
        } else {
            constraints.fill = defaultFillValue;
            this.setBorder( BorderFactory.createEmptyBorder( 1, 1, 1, 1) );
            
        }        

    }//end método checkEmptyLabel

    /**
     * Este método es llamado cuando alguno de los eventos de transición de
     * editor a label es generado. Se encarga de la transcición y del seteo
     * del nuevo texto si es necesario.
     */
    public abstract void deactivateEditMode();

    /**
     * Este método es llamado cuando alguno de los eventos de transcición de
     * label a editor es generado. Se encarga de la transcición, habilitando
     * el editor para la modificación del valor del label.
     */
    public abstract void activateEditMode();
    
    /**
     * Este método es llamado cuando se setea un texto al label, para checkear
     * que el mismo sea válido y modificarlo de ser necesario.
     * @param text texto a setear.
     * @return valor editado (o no) para setear en el label
     */
    protected abstract String preCheck( String text );
    
    /**
     * Handler que se encarga de activar el editor cuando se hace click
     * sobre el label.
     */
    protected class ClickHandler extends MouseAdapter {
        
        /**
         * {@inheritDoc}
         */
        @Override
        public void mouseClicked( MouseEvent e ) {
            if ( isEnabled() )
                activateEditMode();

        }//end método mouseClicked

    }//end clase ClickHandler

    /**
     * Handler que se encarga de pasar al modo de no-edición cuando se presiona
     * la tecla Esc o Enter.
     */
    protected class KeyHandler extends KeyAdapter {

        /**
         * {@inheritDoc}
         */
        @Override
        public void keyPressed( KeyEvent e ) {
            //si recibimos la tecla ESC, salimos del modo edicion
            if ( e.getKeyCode() == KeyEvent.VK_ESCAPE
                 | e.getKeyCode() == KeyEvent.VK_ENTER )
                deactivateEditMode();

        }//end método keyPressed

    }//end clase KeyHandler

    /**
     * Handler que se encarga de desactivar el modo de edición cuando el 
     * componente editor pierde el foco de la aplicación.
     */
    protected class FocusHandler implements FocusListener {

        /**
         * {@inheritDoc}
         */
        @Override
        public void focusGained( FocusEvent e ) {
            //ignore
        }//end método focusGained

        /**
         * {@inheritDoc}
         */
        @Override
        public void focusLost( FocusEvent e ) {
            deactivateEditMode();

        }//end método focusLost

    }//end clase FocusHandler

}//end clase AbstractEditableLabel
